package umg.os.devices.filesystem.logical;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.InputMismatchException;
import java.util.List;
import java.util.Map;
import java.util.Random;

import umg.os.devices.filesystem.basic.BasicFileSystem;
import umg.os.devices.filesystem.basic.BasicFileSystem.DataOversizedException;
import umg.os.devices.filesystem.basic.HDDHandler.VolumeUnavailableException;

public class LogicalFileSystem {

	private Map<Integer, List<Integer>> directoriesStructure;
	private Map<Integer, FileControlBlock> fcbs;
	private FileControlBlock 
	workingDirectory,
	root;
	private BasicFileSystem bfs;

	public LogicalFileSystem(BasicFileSystem bfs) throws IOException, VolumeUnavailableException {
		this.bfs = bfs;
		boolean rootFound = false;
		byte[] metadata = bfs.getLogicalMetadata();
		fcbs = new HashMap<Integer, FileControlBlock>();
		directoriesStructure = new HashMap<Integer, List<Integer>>();
		if (metadata.length % BasicFileSystem.USEFUL_SECTOR_SIZE != 0) {
			throw new InputMismatchException("The data cannot be casted");
		}
		int totalFCBs = ((metadata.length / BasicFileSystem.USEFUL_SECTOR_SIZE) * 2);
		//System.out.println("TOTALFCBS: "+totalFCBs);
		//System.out.println(Integer.toBinaryString((int)metadata[metadata.length - BasicFileSystem.USEFUL_SECTOR_SIZE]));
		if (metadata[metadata.length - BasicFileSystem.USEFUL_SECTOR_SIZE] == (byte)0x00) {
			totalFCBs--;
			//System.err.println("# impar");
		}
		//System.out.println("TOTALFCBS: "+totalFCBs);
		/*int indexOfData = 0;
		for (byte b : metadata) {
			System.out.println((char)b+" : "+(int)b+" - "+indexOfData++);
		}*/
		byte[] dataOfFCB = new byte[FileControlBlock.FILE_CONTROL_BLOCK_SIZE];
		for (int i = 0, offset = 0; i < totalFCBs; i++, offset += FileControlBlock.FILE_CONTROL_BLOCK_SIZE) {
			if (offset % BasicFileSystem.USEFUL_SECTOR_SIZE == 0) {
				offset++;
			}
			System.arraycopy(metadata, offset, dataOfFCB, 0, FileControlBlock.FILE_CONTROL_BLOCK_SIZE);
			FileControlBlock fcb = new FileControlBlock(dataOfFCB);
			//System.out.println("Leido: *******************************************\n"+fcb);
			fcbs.put(new Integer(fcb.getId()), fcb);
			if (!directoriesStructure.containsKey(fcb.getParentID())) { //adding as children the new FCB
				directoriesStructure.put(fcb.getParentID(), new ArrayList<Integer>());
			}
			if (fcb.getParentID() == fcb.getId()) { //ROOT DIRECTORY, this prevents root to contain itself
				if (!rootFound) {
					root = workingDirectory = fcb; //Setting current directory to be the root
					rootFound = true;
					continue;
				} else {
					throw new CorruptedFileStructureException("Multiple roots are not supported");
				}
			}
			directoriesStructure.get(new Integer(fcb.getParentID())).add(fcb.getId());
		}
		System.out.println("FCB's leidos ************************************************");
		for (Map.Entry<Integer, FileControlBlock> entry : fcbs.entrySet()) {
			System.out.println(entry.getValue());
			System.out.println("Tamanio en disco: "+BasicFileSystem.getSizeInDiskFor(entry.getValue().getSize())+" bytes");
			System.out.println("ID's hijos: "+directoriesStructure.get(entry.getKey()));
			System.out.println("Path absoluto: "+getAbsolutePathFor(entry.getKey()));
			System.out.println("************************************************");
		}
	}

	public String getAbsolutePathFor(int id) {
		StringBuffer sb = new StringBuffer();
		int currentID = id;
		if (currentID == fcbs.get(currentID).getParentID()) {
			return java.io.File.separator;
		}
		while (currentID != fcbs.get(currentID).getParentID()) {
			sb.append(java.io.File.separator+fcbs.get(currentID).getName());
			currentID = fcbs.get(currentID).getParentID();
		}
		return sb.toString();
	}

	protected boolean exists(int id) {
		return fcbs.containsKey(id);
	}

	protected byte[] readSectors(int[] sectors) throws IOException, VolumeUnavailableException {
		return bfs.readContinuouslyFromSectors(sectors);
	}
	
	protected boolean isDirectory(FileControlBlock fcb) throws NoSuchFileException  {
		if (!exists(fcb.getId())) {
			throw new  NoSuchFileException("The file "+getAbsolutePathFor(fcb.getId())+ " does not exist!!!");
		}
		return fcb.getFirstSector() == 0;
	}

	protected boolean isDirectory(int id) throws NoSuchFileException {
		return isDirectory(fcbs.get(id));
	}

	protected boolean isFile(FileControlBlock fcb) throws NoSuchFileException {
		return !isDirectory(fcb);
	}

	protected boolean isFile(int id) throws NoSuchFileException {
		return !isDirectory(id);
	}

	public void createFile(File file) throws IOException, VolumeUnavailableException, DataOversizedException {
		if (exists(file.getFcb().getId())) {
			throw new FileAlreadyExistsException("The file: "+getAbsolutePathFor(file.getFcb().getId())+" already exists...");
		}
		Random rn = new Random();
		int 
		id, 
		firstSector = bfs.reserveSector();
		while (fcbs.containsKey((id = rn.nextInt())) || id > 0xFFFFFF || id < 0);
		file.setFirstSector(firstSector);
		file.getFcb().setId(id);
		fcbs.put(new Integer(id), file.getFcb());
		directoriesStructure.get(new Integer(file.getFcb().getParentID())).add(id);
		saveTreeData();
	}
	 
	public void createDirectory(File file) throws IOException, VolumeUnavailableException, DataOversizedException {
		if (exists(file.getFcb().getId())) {
			throw new FileAlreadyExistsException("The file: "+getAbsolutePathFor(file.getFcb().getId())+" already exists...");
		}
		Random rn = new Random();
		int 
		id;
		while (fcbs.containsKey((id = rn.nextInt())) || id > 0xFFFFFF || id < 0);
		file.getFcb().setFirstSector(0);
		file.getFcb().setId(id);
		fcbs.put(new Integer(id), file.getFcb());
		List<Integer> childrenOfParentFolder = directoriesStructure.get(new Integer(file.getFcb().getParentID()));
		if (childrenOfParentFolder == null) {
			childrenOfParentFolder = new ArrayList<Integer>();
			directoriesStructure.put(new Integer(file.getFcb().getParentID()), childrenOfParentFolder);
		}
		childrenOfParentFolder.add(new Integer(id));
		directoriesStructure.put(new Integer(file.getFcb().getId()), new ArrayList<Integer>());
		saveTreeData();
	}

	public File loadFile(int id) throws FileNotFoundException, IOException, VolumeUnavailableException, DataOversizedException {
		FileControlBlock fcb = fcbs.get(id);
		if (fcb == null) {
			throw new FileNotFoundException(getAbsolutePathFor(id));
		}
		return loadFile(fcb);
	}

	public File loadFile(FileControlBlock fcb) throws IOException, VolumeUnavailableException, DataOversizedException {
		List<Integer> listOfSectors = bfs.getLinkedSectors(fcb.getFirstSector());
		File fileToReturn = new File(bfs, fcb, listOfSectors);
		saveTreeData();
		return fileToReturn;
	}

	public void deleteFile(File fileToDelete) throws IOException, VolumeUnavailableException, DataOversizedException {
		int idToDelete = fileToDelete.getFcb().getId();
		if (!exists(idToDelete)) {
			throw new NoSuchFileException(getAbsolutePathFor(idToDelete));
		}
		if (directoriesStructure.containsKey(idToDelete) || isDirectory(idToDelete)) {
			throw new NoSuchFileException(getAbsolutePathFor(idToDelete));
		} else {
			for (Integer sector : fileToDelete.getDataSectors()) {
				bfs.clearSectorPointer(sector);
			}
			fcbs.remove(idToDelete);
			directoriesStructure.get(new Integer(fileToDelete.getFcb().getParentID())).remove(idToDelete);
		}
		saveTreeData();
	}
	
	public void deleteDirectory(File directoryToDelete) throws IOException, VolumeUnavailableException, DataOversizedException {
		int idToDelete = directoryToDelete.getFcb().getId();
		if (!exists(idToDelete)) {
			throw new NoSuchFileException(getAbsolutePathFor(idToDelete));
		}
		if (!directoriesStructure.containsKey(idToDelete) || isFile(idToDelete)) {
			throw new NotDirectoryException(getAbsolutePathFor(idToDelete));
		} else {
			if (directoriesStructure.get(new Integer(idToDelete)).size() > 0) {
				throw new DirectoryNotEmptyException(getAbsolutePathFor(directoryToDelete.getFcb().getId()));
			}
			else {
				fcbs.remove(idToDelete);
				directoriesStructure.remove(idToDelete);
			}
		}
		saveTreeData();
	}
	
	public File getFile(String path) throws FileNotFoundException, IOException, VolumeUnavailableException, DataOversizedException {
		int id;
		try {
			return loadFile(getIDForPath(path));
		} catch (NoSuchFileException nsfe) {
			id = getParentIDForPath(path);
		}
		String[] names = path.split(java.io.File.separator);
		FileControlBlock fcb = new FileControlBlock(names[names.length-1], id);
		return new File(bfs, fcb, new ArrayList<Integer>());
	}
	
	public int getParentIDForPath(String path) throws NoSuchFileException {
		FileControlBlock fcb = root;
		if (!path.startsWith(java.io.File.separator)) {
			return workingDirectory.getId();
		}else {
			String[] names = path.split(java.io.File.separator);
			boolean valid = false;
			for (int i = 0; i < (names.length - 1) ; i++) {
				if (names[i] == null || names[i].length() == 0) {
					continue;
				}
				for (Integer j : directoriesStructure.get(new Integer(fcb.getParentID()))) {
					if (fcbs.get(j).getName().equals(names[i])) {
						fcb = fcbs.get(j);
						valid = true;
						break;
					}
				}
				if (!valid) {
					throw new NoSuchFileException("The directory "+names[i]+" does not exists!!!");
				}
				valid = false;
			}
		}
		return fcb.getId();
	}
	
	public int getIDForPath(String path) throws NoSuchFileException {
		FileControlBlock parent = fcbs.get(getParentIDForPath(path));
		//System.out.println("Su padre es: "+parent.getName());
		if (!path.startsWith(java.io.File.separator)) {
			path = getAbsolutePathFor(workingDirectory.getId()) + path;
		}
		String[] names = path.split(java.io.File.separator);
		String fileName = names[names.length-1];
		for (Integer childID : directoriesStructure.get(new Integer(parent.getId()))) {
			/*System.out.println("Comparando con los hijos del padre, el archivo es: \n\""+fileName+"\"");
			for (byte b : fileName.getBytes()) {
				System.out.print(b+',');
			}
			System.out.println("\nel hijo comparado es: \n\""+fcbs.get(childID).getName()+"\"");
			for (byte b : fcbs.get(childID).getName().trim().getBytes()) {
				System.out.print(b+',');
			}
			System.out.println("\n valor de la comparacion "+fcbs.get(childID).getName().equals(fileName));
			*/
			if (fcbs.get(childID).getName().equals(fileName)) {
				return childID;
			}
		}
		throw new NoSuchFileException("The file "+fileName+" does not exists!!!");
	}
	
	public void setWorkingDirectory(FileControlBlock workingDirectory) {
		this.workingDirectory = workingDirectory;
	}
	
	public void setWorkingDirectory(int id) throws NoSuchFileException {
		setWorkingDirectory(fcbs.get(id));
	}
	
	public void setWorkingDirectory(String path) throws NoSuchFileException {
		setWorkingDirectory(getIDForPath(path));
	}
	
	public FileControlBlock getWorkingDirectory() {
		return workingDirectory;
	}

	public static void installSO(BasicFileSystem bfs) throws DataOversizedException, IOException, VolumeUnavailableException {
		System.out.println("Installing SO...");
		FileControlBlock root = FileControlBlock.generateRootDirectory();
		byte[] 
				rootAsBytes = root.getDataRepresentation(),
				dataToSave = new byte[rootAsBytes.length + 1]; 
		dataToSave[0] = (byte)0x00;
		System.arraycopy(rootAsBytes, 0, dataToSave, 1, rootAsBytes.length);
		bfs.writeSector(0, dataToSave); //Root is in "sector zero" 
		System.out.println("Installed...");
	}

	protected void saveTreeData() throws IOException, VolumeUnavailableException, DataOversizedException {
		byte[] data = new byte[(int) (Math.ceil(((float)fcbs.size() / 2f)) * BasicFileSystem.USEFUL_SECTOR_SIZE)];
		int offset = 0;
		for (Map.Entry<Integer, FileControlBlock> fcb : fcbs.entrySet()) {
			if (offset % BasicFileSystem.USEFUL_SECTOR_SIZE == 0) {
				data[offset] = (byte) 0x01;
				offset++;
			}
			System.arraycopy(fcb.getValue().getDataRepresentation(), 0, data, offset, FileControlBlock.FILE_CONTROL_BLOCK_SIZE);
			offset += FileControlBlock.FILE_CONTROL_BLOCK_SIZE;
		}
		if (fcbs.size() % 2 != 0) {
			data[data.length - BasicFileSystem.USEFUL_SECTOR_SIZE] = (byte) 0x00;
		}
		bfs.saveLogicalMetadata(data);
	}
	
	@SuppressWarnings("serial")
	public class CorruptedFileStructureException extends RuntimeException {

		private CorruptedFileStructureException() {

		}

		private CorruptedFileStructureException(String message) {
			super(message);
		}

	}

}
